<?php
// +----------------------------------------------------------------------
// | Bwsaas
// +----------------------------------------------------------------------
// | Copyright (c) 2015~2020 http://www.buwangyun.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Gitee ( https://gitee.com/buwangyun/bwsaas )
// +----------------------------------------------------------------------
// | Author: buwangyun <hnlg666@163.com>
// +----------------------------------------------------------------------
// | Date: 2020-9-28 10:55:00
// +----------------------------------------------------------------------

namespace buwang\service;

use app\common\model\member\Wallet;
use app\common\model\MemberMiniapp;
use app\common\model\MemberMiniappAudit;
use app\common\model\MemberMiniappOrder;
use app\common\model\MemberMiniappTemplate;
use app\common\model\Miniapp;
use app\manage\model\admin\MiniappModule;
use app\manage\model\AuthGroup;
use app\manage\model\AuthGroupAccess;
use app\manage\model\AuthGroupNode;
use app\manage\model\AuthNode;
use buwang\util\File;
use buwang\util\Sql;
use buwang\util\Util;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\Exception;
use think\facade\Db;
use app\manage\model\Member;
use app\manage\model\Config;
use app\manage\model\ConfigTab;
use app\manage\model\ConfigGroup;
use app\manage\model\ConfigGroupData;
use buwang\exception\MiniappException;


/**
 * 应用服务类
 * Class PluginService
 * @package buwang\service
 */
class MiniappService
{
    /**
     * 安装应用
     * @param string $dir 应用目录
     * @param bool $importDemoData 是否导入测试数据
     * @return bool
     * @throws Exception
     */
    public static function install(string $dir, bool $importDemoData = false)
    {
        if (empty($dir)) throw new Exception('应用目录不能为空');
        //检查应用表中是否已存在该应用记录，如果有说明已安装
        $app = Miniapp::where('dir', $dir)->find();
        if (!empty($app)) throw new Exception('应用已安装,禁止重复安装');
        //定义一个常量，安装应用的目录
        define('MINIAPP_DIR', $dir);
        //找到应用描述配置
        $version = self::getVersion($dir);
        if (empty($version) || !isset($version['types']) || !isset($version['version']) || !isset($version['database']) || !isset($version['title'])) {
            throw new Exception('未找到/config/version.php应用必要配置信息');
        }
        //设置应用数据表前缀常量 MINIAPP_DATABASE_PREFIX
        set_miniapp_database_prefix($dir);

        //节点文件menu.php是否完整
        $group_php_path = base_path($dir) . DS . "install" . DS . "menu.php";
        if (!file_exists($group_php_path)) throw new Exception('install/menu.php文件不存在');
        $group_php_data = include $group_php_path; //用户组=>功能节点配置数据
        if (empty($group_php_data)) throw new Exception('install/menu.php文件不能为空');
        $hasBaseModule = 0; //基础功能模块的数量
        //累计统计应用组配置文件中的基础功能模块数量
        foreach ($group_php_data as &$item) {
            $item['module_type'] = 1;
            $hasBaseModule++;
        }
        //必须且只能有一个基础功能组
        if ($hasBaseModule == 0) throw new Exception('应用必须有至少一个基础功能');
        //组装插入系统应用表的记录数据
        //插入一条数据
        $data = [
            'dir' => $dir,          //目录
            'types' => $version['types'], //应用类型,多值，号隔开
            'is_manage' => $version['is_manage'], //平台管理:0=关闭,1=开启'
            'is_openapp' => $version['is_openapp'],//0=独立应用,1=开放平台',
            'title' => $version['title'],  //应用名称
            'version' => $version['version'], //应用当前版本
            'is_wechat_pay' => $version['is_wechat_pay'],//微信支付:0=关闭,1=开启
            'is_alipay_pay' => $version['is_alipay_pay'],//支付宝支付:0=关闭,1=开启
            'describe' => $version['describe'], //应用描述
            'content' => $version['describe'], //应用详情介绍
            'template_id' => 0, //小程序代码库中的代码模板初始
            'sell_price' => 0,  //应用销售价【已经弃用，现在应用分为应用套餐，以套餐来定价】
            'market_price' => 0, //应用市场价【已经弃用，现在应用分为应用套餐，以套餐来定价】
            'expire_day' => 0,  //体验天数【已经弃用，现在应用分为应用套餐，以套餐来定】
            'logo_image' => DS . 'static' . DS . strtolower($dir) . DS . 'logo.png', //应用logo
            'qrcode_image' => '',  //二维码
            'style_images' => DS . 'static' . DS . strtolower($dir) . DS . 'style.png', //应用截图
        ];

        try {
            //2.应用表增加数据
            $app = Miniapp::create($data);

            // 3.应用自定义数据表删除
            //TODO 20230513 需要考虑使用插件数据库迁移工具，方便应用升级 composer require topthink/think-migration
            self::runSQL($dir, 'install', 'install'); //<----执行应用安装sql（应用install目录下的sql，正则 '；'号切割语句，逐条执行），插入表和应用需要的配置和数据组，但该配置和数据组只是初始化配置（member_id=0的数据），并未实际给租户插入配置和数据组
            //4.系统表打入数据
            //角色表 应用功能表
            self::importGroupData($dir); //<----导入应用角色组和应用菜单权限节点数据
            //节点表
            //self::importNodeData($dir);
            //5.是否安装测试数据 （非demo应用不会进该判断）
            if ($importDemoData) self::runSQL($dir, 'install', 'test');
            //调用安装事件
            event('MiniappInstall', [$app]); // <----事件监听中，会调用配置安装方法 ，根据安装sql插入的那些初始化数据，对当前平台下每个租户插入应用需要的租户配置和租户组合数据
            //1.拷贝静态资源文件 app/demoApp/install/static/ => public/static/demoApp/
            if (file_exists(root_path() . 'app' . DS . $dir . DS . 'install' . DS . 'static' . DS)) {
                //拷贝模板到前台模板目录中去 public/static/demoApp/
                File::copy_dir(root_path() . 'app' . DS . $dir . DS . 'install' . DS . 'static' . DS, root_path() . 'public' . DS . 'static' . DS . strtolower($dir) . DS);
            }

        } catch (\Throwable $e) {
            throw new Exception("安装失败：" . $e->getMessage());
        }
        return true;
    }

    /**
     * 卸载应用
     * @param string $dir 是否强制卸载
     * @param bool $force
     * @return bool
     * @throws Exception
     * @throws DataNotFoundException
     * @throws DbException
     * @throws ModelNotFoundException
     */
    public static function uninstall(string $dir, bool $force = false): bool
    {
        if (empty($dir)) throw new Exception('应用目录不能为空');
        $app = Miniapp::where('dir', $dir)->find();
        if (!$app) throw new Exception('应用未安装');
        //应用已被购买  则无法卸载  只能禁用
        $member_mini_app = MemberMiniapp::where('miniapp_id', $app['id'])->find();
        if ($member_mini_app && !$force) throw new Exception('应用已售出,无法卸载');
        try {
            //删除静态资源 public/static/demoApp/
            if (file_exists(root_path() . 'public' . DS . 'static' . DS . $dir . DS)) {
                //拷贝模板到前台模板目录中去 public/static/demoApp/
                File::del_dir(root_path() . 'public' . DS . 'static' . DS . $dir . DS);
            }
            $groups_str = MiniappModule::where('miniapp_id', $app['id'])->value('group_id');
            //节点中间表删除
            AuthGroupNode::where('group_id', 'in', $groups_str)->delete();
            //节点表删除
            AuthNode::where('app_name', $dir)->delete();
            //应用功能表删除
            MiniappModule::where('miniapp_id', $app['id'])->delete();
            //角色表删除
            AuthGroup::where('id', 'in', $groups_str)->whereOr('app_name', $dir)->delete();
            //权限角色表
            AuthGroupAccess::where('group_id', 'in', $groups_str)->delete();
            //TODO 删除租户应用表
            MemberMiniapp::where('miniapp_id', $app['id'])->delete();
            //TODO 删除应用购买记录表
            MemberMiniappOrder::where('miniapp_id', $app['id'])->delete();
            //TODO 删除应用模板上架记录
            if ($member_mini_app) {
                MemberMiniappAudit::where('member_miniapp_id', $member_mini_app['id'])->delete();
            }
            //TODO 删除应用同步微信第三方模板库记录
            MemberMiniappTemplate::where('miniapp_id', $app['id'])->delete();
            //应用自定义数据表删除
            //TODO 需要考虑使用插件数据库迁移工具，方便应用升级降级 composer require topthink/think-migration
            self::runSQL($dir, 'uninstall', 'uninstall');
            //应用表记录删除
            $app->delete();
        } catch (\Throwable $e) {
            throw new Exception("卸载失败：" . $e->getFile() . $e->getLine() . $e->getMessage());
        }

        return true;
    }

    /**
     * 购买应用功能
     * @param int $user_id 租户id
     * @param int $price_info 应用购买下单数据 $price_info['miniapp_module_id'] 应用套餐id $price_info['price_id'] 规格id
     * @param string $miniapp_name_diy 自定义应用名
     * @param bool $money_change 是否扣除资金，可用来判断购买来源 true:从应用市场购买 false:从插件市场购买
     * @throws Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public static function buy($user_id, $price_info, $miniapp_name_diy = '', $money_change = true)
    {
        $miniapp_module_id = $price_info['miniapp_module_id']; //应用套餐id
        $price_id = $price_info['price_id'];           //价格id
        //NOTE 功能是否存在
        $module = MiniappModule::where(['id' => $miniapp_module_id, 'status' => 1])->find($miniapp_module_id);
        if (!$module) throw new Exception('套餐不存在或已下架！');
        //判断是否有套餐参数
        if (!$module['price_list']) throw new Exception('该套餐数据错误，缺少规格参数！');
        //查找该规格的套餐
        $price_module = MiniappModule::priceFind($miniapp_module_id, $price_id);
        //TODO 新版本采用了索引值来获取套餐规格信息
        if (!$price_module && isset($module['price_list'][$price_id])) {
            $price_module = $module['price_list'][$price_id];
        }
        if (!$price_module) throw new Exception('不存在对应的套餐规格');
        //如果功能不是基础功能  查看基础功能是否已购买【1为基础功能 2为附加功能 3 为购买插件】
        if ($module['type'] == 3 && $money_change) throw new Exception('插件功能请前往插件市场购买');
        //查找应用
        $miniapp = \app\common\model\Miniapp::where(['id' => $module['miniapp_id'], 'status' => 1])->field('id,dir,title')->find();
        //应用不存在
        if (!$miniapp) throw new Exception('应用不存在或已关闭购买通道');

        try {
            //NOTE 扣余额 记录账变
            if ($money_change && $price_module['price'] > 0) {
                //余额判断
                $user_money = Wallet::where(['member_id' => $user_id])->lock(true)->value('money');
                if ($money_change > $user_money) throw new Exception('您余额不足，请先充值');

                $fee = Wallet::changeMoney($user_id, 'miniapp', 0 - $price_module['price'], '购买应用功能');
                if (!$fee) throw new \Exception(\app\common\model\member\Wallet::getError());
            } else {
                throw new Exception('购买金额不正确！');
            }

            //NOTE 增加应用购买订单记录
            $orderParam = [
                'member_id' => $user_id,        //租户id
                'miniapp_id' => $miniapp['id'], //应用id
                'miniapp_module_id' => $miniapp_module_id, //模块id
                'title' => $miniapp['title'],   //应用名
                'update_var' => 0,              //小程序模板ID 0
                'price' => $price_module['price'],    //模块规格价格
                'valid_days' => $price_module['day'], //有效天数 = 当前规格有效天数
                'expire_time' => $price_module['day'] > 0 ? time() + $price_module['day'] * 24 * 60 * 60 : 0, //过期时间
                'miniapp_module_type' => $module['type'], //TODO 得到功能类型【被新版本弃用】
            ];
            $order = MemberMiniappOrder::create($orderParam);
            //NOTE 增加用户应用表bw_member_miniapp记录
            $memberMiniapp = MemberMiniapp::where(['member_id' => $user_id, 'miniapp_id' => $miniapp['id']])->find();
            if (!$memberMiniapp) {
                //是否执行购买钩子
                $have_buy_hook = true;
                //新增
                $memberMiniapp = new MemberMiniapp;
                $service_id = md5($order['id'] . mt_rand(1000, 9999));
                $memberMiniappParam = [
                    'service_id' => $service_id,
                    'miniapp_order_id' => $order['id'],
                    'member_id' => $user_id,
                    'miniapp_id' => $miniapp['id'],
                    'appname' => $miniapp_name_diy ?: $miniapp['title'],
                    'template_id' => isset($miniapp['template_id']) ? $miniapp['template_id'] : 0,
                    'version' => isset($miniapp['version']) ? $miniapp['version'] : '1.0.0',
                    'expire_time' => $price_module['day'] > 0 ? time() + $price_module['day'] * 24 * 60 * 60 : 0, //过期时间
                ];
                $memberMiniapp->save($memberMiniappParam);
            } else {
                //是否执行购买钩子
                $have_buy_hook = false;
                //更新
                $memberMiniapp->miniapp_order_id = $order['id'];
                $memberMiniapp->expire_time = $price_module['day'] > 0 ? time() + $price_module['day'] * 24 * 60 * 60 : 0;//过期时间
                $memberMiniapp->save();
            }

            //NOTE 菜单增加 添加角色组
            //查询所有角色
            $where = [['id', 'in', $module['group_id']], ['group_name', 'in', $module['group_name']]];
            $authGroups = AuthGroup::where('type', 'app')
                ->where(function ($query) use ($where) {
                    $query->whereOr($where);
                })
                ->select()->toArray();
            if (empty($authGroups)) throw new Exception('套餐功能组，数据异常');

            $new_group_id = [];
            foreach ($authGroups as $authGroup) {
                //TODO 新版本组装新的group_id，防止应用被重新安装后，group_id变化导致查询不到功能组
                $new_group_id[] = $authGroup['id'];
                $groupAccessParam[] = [
                    'uid' => $user_id, //租户id
                    'group_id' => $authGroup['id'], //角色组id
                    'name' => $authGroup['name'], //角色名
                    'scopes' => 'member'  //租户member 管理后台admin
                ];
            }

            if (!empty($groupAccessParam)) {

                //查询所有应用功能角色套餐
                //$appGroupsIds = AuthGroup::where('app_name','=',$miniapp['dir'])->where('type','app')->column('id');
                //TODO 购买应用角色先执行删除再添加，防止数据冲突
                AuthGroupAccess::where('uid', '=', $user_id)
                    ->where('group_id', 'in', $new_group_id)
                    ->where('scopes', '=', 'member')
                    ->delete();
                $authGroupAccess = new AuthGroupAccess();
                $authGroupAccess->saveAll($groupAccessParam);
            }
            //设置一下应用数据表前缀
            set_miniapp_database_prefix($miniapp['dir']);

            //初次购买调用购买事件
            if ($have_buy_hook) event('MiniappBuySuccess', [$user_id, $miniapp, $module, $memberMiniapp]);
        } catch (\Throwable $e) {
            throw new Exception($e->getFile() . $e->getLine() . $e->getMessage());
        }
    }

    public static function pluginBuy($user_id, $plugin, $group_ids_arr = [])
    {
        try {
            list($scopes, $type) = explode('_', $plugin['type']);
            if ($scopes == 'member') {
                //查找应用
                $miniapp = \app\common\model\Miniapp::where(['dir' => $type, 'status' => 1])->field('id,dir,title')->find();
                //应用不存在
                if (!$miniapp) throw new Exception('插件所属应用不存在或已关闭购买通道');
            }
            //NOTE 菜单增加 添加角色组
            //查询所有角色
            $authGroups = AuthGroup::where('id', 'in', $group_ids_arr)->where('type', 'plugin')->select()->toArray();
            foreach ($authGroups as $authGroup) {
                $groupAccessParam[] = [
                    'uid' => $user_id, //租户id
                    'group_id' => $authGroup['id'], //角色组id
                    'name' => $authGroup['name'], //角色名
                    'scopes' => $authGroup['scopes']  //租户member 管理后台admin
                ];
            }
            if (!empty($groupAccessParam)) {

                //查询所有应用功能角色套餐
                //$appGroupsIds = AuthGroup::where('app_name','=',$miniapp['dir'])->where('type','app')->column('id');
                //购买应用角色先执行删除再添加，防止数据冲突
                AuthGroupAccess::where('uid', '=', $user_id)->where('group_id', '=', $authGroup['id'])->where('scopes', '=', 'member')->delete();
                $authGroupAccess = new AuthGroupAccess();
                $authGroupAccess->saveAll($groupAccessParam);
            }
            //触发插件购买事件
            event('PluginBuySuccess', [$user_id, $plugin, []]);

        } catch (\Throwable $e) {
            throw new Exception($e->getMessage());
        }
    }

    /**
     * 获取应用配置信息
     * @param string $dir
     * @return array|mixed
     */
    public static function getVersion(string $dir)
    {
        $miniapp_config = root_path() . 'app' . DS . $dir . DS . 'config' . DS . 'version.php';
        $config = [];
        if (is_file($miniapp_config)) {
            $config = include $miniapp_config;
        }
        return $config;
    }

    /**
     * 执行数据库脚本
     * @param string $name 应用目录
     * @param string $dir 数据库脚本所在目录,默认install
     * @param string $sqlName 数据库脚本名称,默认install
     * @return bool
     * @throws Exception
     */
    public static function runSQL(string $name = '', string $dir = 'install', string $sqlName = 'install')
    {
        //数据库安装脚本文件地址 app/demoApp/install/install.sql
        //数据库卸载脚本文件地址 app/demoApp/uninstall/uninstall.sql
        $sql_file = base_path($name) . DS . "{$dir}" . DS . "{$sqlName}.sql";
        if (file_exists($sql_file)) {
            $table_prefix = config('database.connections.mysql.prefix');
            $sql_statement = Sql::getSqlFromFile($sql_file, false, [], $table_prefix);
            if (!empty($sql_statement)) {
//                //修改表前缀
//                $table_prefix = config('database.connections.mysql.prefix');
//                if (!empty($table_prefix)) {
//                    $sql_statement = str_replace('bw_', $table_prefix, $sql_statement); //兼容老版本
//                    $sql_statement = str_replace('__BWPREFIX__', $table_prefix, $sql_statement);
//                }

                foreach ($sql_statement as $value) {
                    try {
                        Db::execute($value);
                    } catch (\Exception $e) {
                        throw new Exception($e->getMessage());
                    }
                }
            }
        }
        return true;
    }

    /**
     * @param string $dir 路径
     * @param array $except 排除项
     * @return array
     * 搜索给定地址下目录列表
     */
    public static function getDir(string $dir, array $except)
    {
        $dirArray[] = NULL;
        if (false != ($handle = opendir($dir))) {
            $i = 0;
            while (false !== ($file = readdir($handle))) {
                //去掉"“.”、“..”以及带“.xxx”后缀的文件
                if (array_search($file, $except) === false && $file != ".htaccess" && $file != "." && $file != ".." && !strpos($file, ".")) {
                    $dirArray[$i] = $file;
                    $i++;
                }
            }
            //关闭句柄
            closedir($handle);
        }
        return $dirArray;
    }

    /**
     * 创建应用权限菜单节点.
     *
     * @param string $name
     *
     * @return void
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public static function createAppMenu(string $name) {
        self::importGroupData($name);
    }

    /**
     * 导入角色数据
     * @param string $name
     * @return bool
     */
    private static function importGroupData(string $name): bool
    {
        //检查是否有menu.php，如果没有菜单配置则不作菜单插入
        $path = base_path($name) . DS . "install" . DS . "menu.php";
        if (!file_exists($path)) return true;
        $data = include $path; //得到角色组和菜单节点树
        if (empty($data)) return true;

        //获取所有应用
        $miniapps = array_flip(Miniapp::column('dir', 'id')); //以目录名为key 应用id为value的数组
        //应用模块记录 如果应用没有安装进表中则抛错
        if (!isset($miniapps[$name])) throw new Exception("{$name}应用不存在");

        //待插入应用功能
        //$modules = [];
        foreach ($data as $item) {
            //遍历最外层  = 角色组[也叫应用功能组]
            //组装角色组数据
            $groupParam = [
                //角色名称,展示在角色列表中
                'name' => $item['name'],
                //角色唯一标识,不可重复
                'group_name' => strpos($item['group_name'], $name) !== false ? $item['group_name'] : $name . '_' . $item['group_name'],
                //角色备注
                'remark' => $item['remark'] ?: '',
                //登陆类型 admin=总后台,member=租户后台
                'scopes' => 'member',  //目前应用属于租户，则角色组只可能是租户后台的，即member
                'type' => 'app',//角色组类型：system=系统，app=应用，plugin=插件
                'app_name' => $name,//应用名称 取目录名
                'member_id' => 0//TODO 租户用户id（0表示系统角色），如果租户购买该应用后，会在数据库表bw_auth_group_access，检查租户对应的用户权限角色组
            ];
            //查看该根节点是否存在，如果存在则更新，不存在则新增
            $group = AuthGroup::where($groupParam)->find();
            if (!$group) {
                $group = AuthGroup::create($groupParam); //创建角色组
                if (!$group) throw new Exception("{$item['name']}角色导入失败");
            }

            //TODO 如果每个功能组有节点则插入节点
            if (isset($item['nodes']) && !empty($item['nodes'])) {
                self::importNodeData($name, $group, $item['nodes']); //目录名 ，角色组，菜单权限节点树
            }
        }

        return true;
    }

    /**
     * 导入节点表
     * @param $name //应用名
     * @param $group //当前新增的功能角色组
     * @param $nodes //当前功能角色组的权限菜单节点【数组】
     * @return bool
     * @throws Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public static function importNodeData($name, $group, $nodes): bool  //目录名 ，角色组，菜单权限节点树
    {
        //为该应用菜单建立根节点,应用管理下,有则取出
        $rootNodeParam = [
            "pid" => 0,//上级菜单id,填0代表是顶部选项卡
            "title" => (self::getVersion($name))['title'],//菜单名称 根节点菜单名从应用配置中取title
            "app_name" => $name,//应用/插件名
            "type" => 'app',//权限分类 系统/应用/插件
            "menu_path" => $name,//菜单/节点访问url
            "name" => $name,//TP框架中真实的路由
            "auth_name" => $name,//权限标识,必填 唯一
            "param" => '',//参数
            "target" => '_self',//打开方式
            "ismenu" => 1,//是否菜单
            "icon" => '',//图标 根节点默认无图标
            "remark" => '',//备注
            "scopes" => 'member'//scopes
        ];
        //查看该根节点是否存在，如果存在则更新，不存在则新增
        $rootNode = AuthNode::where($rootNodeParam)->find();
        if (!$rootNode) $rootNode = AuthNode::create($rootNodeParam);

        //把根节点导入该角色组中  插入角色权限中间表中（把该权限节点赋给该角色）
        $groupNodeParam = [
            'group_id' => $group['id'],       //组id
            'node_id' => $rootNode['id'],     //节点id
            'node_name' => $rootNode['name'], //节点后台url
            'auth_name' => $rootNode['auth_name'],//规则唯一英文标识
            'type' => 'app',
        ];
        AuthGroupNode::create($groupNodeParam); //中间节点创建
        //执行该角色组下所有权限节点的插入和赋予权限角色
        self::importNode($name, $group, $nodes, $rootNode['id']); //应用目录名，角色组，规则唯一英文标识，根节点id（所有该角色的节点都会插入到该节点下面）

        return true;
    }

    /**
     * 导入节点与角色节点中间表
     * @param array $groups
     * @param array $data
     * @param int $pid 1172是应用管理的节点id
     * @return bool
     * @throws Exception
     */
    private static function importNode($name, $group, $nodes, $pid)
    {
        //TODO 2021/7/2 修复菜单插入顺序错误： 本级菜单倒序插入，因为是按照id大到小排列，所以最靠前的菜单要最后插入
        $nodes = array_reverse($nodes);
        //$name应用目录名，$group角色组，$nodes规则唯一英文标识，$pid根节点id（所有该角色的节点都会插入到该节点下面）
        foreach ($nodes as $item) {
            //遍历 多维数组 本层级数组 的节点（$nodes 是树形结构，多维数组）
            //插入节点
            $nodeParam = [
                "pid" => $pid,//上级菜单id,填0代表是顶部选项卡
                "title" => $item['title'],//菜单名称
                "app_name" => $name,//应用/插件名
                "type" => 'app',//权限分类 系统/应用/插件
                "menu_path" => $item['menu_path'],//后台url
                "name" => $item['name'],//后台url
                "auth_name" => $item['auth_name'],//权限标识,必填
                "param" => isset($item['param']) ? $item['param'] : '',//参数
                "target" => isset($item['target']) ? $item['target'] : '_self',//打开方式
                "ismenu" => isset($item['ismenu']) ? $item['ismenu'] : 0,//是否菜单
                "icon" => isset($item['icon']) ? $item['icon'] : '',//图标
                "remark" => isset($item['remark']) ? $item['remark'] : '',//备注
                "sort" => isset($item['sort']) ? $item['sort'] : 0,//导入菜单文件支持设置权重属性
                "scopes" => 'member'//scopes
            ];
            $node = AuthNode::create($nodeParam); //执行插入节点
            if (!$node) throw new Exception("{$item['title']}节点插入失败");

            //插入节点中间表
            $groupNodeParam = [
                'group_id' => $group['id'],
                'node_id' => $node['id'],
                'node_name' => $node['name'],
                'auth_name' => $node['auth_name'],
                'type' => 'app',
            ];
            AuthGroupNode::create($groupNodeParam); //执行插入节点中间表
            //如果存在子节点则继续遍历下一层子节点继续添加节点和赋予权限 （递归调用importNode方法）
            if (isset($item['children']) && !empty($item['children'])) self::importNode($name, $group, $item['children'], $node['id']);
        }
        return true;
    }


    /**
     * 获取应用配置信息
     * @param string $dir
     * @return array|mixed
     */
    public static function getAppConfig(string $dir)
    {
        $miniapp_config = root_path() . 'app/' . $dir . '/install/bw_config.php';
        $config = [];
        if (is_file($miniapp_config)) {
            $config = include $miniapp_config;
        }
        return $config;
    }

    /**
     * 获取应用组合数据信息
     * @param string $dir
     * @return array|mixed
     */
    public static function getAppData(string $dir)
    {
        $miniapp_config = root_path() . 'app/' . $dir . '/install/bw_data.php';
        $config = [];
        if (is_file($miniapp_config)) {
            $config = include $miniapp_config;
        }
        return $config;
    }

    /**
     * 获取应用事件配置信息
     * @param string $dir
     * @return array|mixed
     */
    public static function getEventData(string $dir)
    {
        $addon_dir = 'addons';
        //批量搜索全部事件配置
        if ($dir == CALL_ALL) {
            $regular_addon = root_path() . "{$addon_dir}/*/install/listen.php";
            $regular = root_path() . "app/*/install/listen.php";
            $files = glob($regular);  //根据表达式匹配返回相匹配的文件列表
            $files_addon = glob($regular_addon);  //根据表达式匹配返回相匹配的文件列表
            $files = array_merge($files, $files_addon);
            $config = [];
            foreach ($files as $file) {
                if (is_file($file)) {
                    $config[] = include $file;
                }
            }
        } else {
            $miniapp_config = root_path() . "app/{$dir}/install/listen.php";
            $miniapp_addon = root_path() . "{$addon_dir}/{$dir}/install/listen.php";
            $config = [];
            if (is_file($miniapp_config)) {
                $config[] = include $miniapp_config;
            }
            if (is_file($miniapp_addon)) {
                $config[] = include $miniapp_addon;
            }
        }
        return $config;
    }

    /**
     * 调用应用事件
     * @param string $dir
     * @param $event //事件数据
     * @param $event_name //事件名
     * @return array|mixed
     */
    public static function handle($dir, $event, $event_name)
    {
        //查询是否有应用事件
        $Event = self::getEventData($dir);
        if ($Event) {
            $list = $Event;
            foreach ($list as $event_item) {
                $class = null;
                $method = null;
                $scope = 'common';
                if (isset($event_item[$event_name]['class']) && $event_item[$event_name]['class']) $class = $event_item[$event_name]['class'];
                if (isset($event_item[$event_name]['method']) && $event_item[$event_name]['method']) $method = $event_item[$event_name]['method'];
                if (isset($event_item[$event_name]['scope']) && $event_item[$event_name]['scope'] && in_array($event_item[$event_name]['scope'], ['static', 'object', 'common'])) $scope = $event_item[$event_name]['scope'];
                if ($method) {
                    //方法调用
                    switch ($scope) {
                        case "common":  //公共函数
                            $method($event);
                            break;
                        case "static":  //类静态函数
                            if ($class) {
                                $class::$method($event);
                            }
                            break;
                        case "object":  //类对象函数
                            if ($class) {
                                (new $class)->$method($event);
                            }
                            break;
                    }
                }
            }
        }

    }

    /**
     * 安装应用配置和租户数据
     * @param string $dir
     * @return array|mixed
     */
    public static function installConfigAndGroupData($dir, $trans = false)
    {
        $config = self::getAppConfig($dir);//需要插入的配置数据
        $config_data = self::getAppData($dir);  //需要插入的组合数据
        $config_insert_list = [];//添加的配置数据
        $group_data_insert_list = [];//添加的组合数据
        $config_tabs = [];
        $group_names = [];
        $member_ids = Member::where('parent_id', 0)->column('id');//所有顶级租户id
        //配置
        if ($config) {  //判断应用是否存在配置数据
            if (isset($config['config_name']) && $config['config_name']) {
                //如果存在配置数据，进行租户插入逻辑
                $config_names = $config['config_name'];//得到所有的配置组 （二维数组 ， 一维下标是配置组标识 ，二维是配置组下的每一个配置标识）
                //检查配置分类是否有缺少
                $config_tabs = array_keys($config_names); //得到所有配置分类标识数组
                //判断 当前应用 安装配置文件中所记录的配置标识数量，是否与实际存在于数据库中的应用配置组数量一致 ，如果不一致，说明应用sql执行错误，数据库与实际应安装配置分类不一致。
                $config_tab_ids = ConfigTab::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->where('scopes', 'member')->column('id', 'tab_name');
                if (count($config_tabs) != count($config_tab_ids)) throw new Exception("与安装期望参数不符:在bw_config.php中配置分类数量与实际安装参数不一致");
                //遍历配置分类，给租户插入配置数据
                foreach ($config_tab_ids as $tab_name => $tab_id) {
                    //检测该配置分类是否与配置文件中所写的分类一致
                    if (!$config_names[$tab_name]) throw new Exception("与安装期望参数不符:未在bw_config.php中{$tab_name}配置分类下写入配置");
                    //得到该分类下 应用安装sql 执行后插入的那些 初始化配置，并比对与配置文件中所写的那些配置是否一致
                    $config_list = Config::where('member_id', 0)->where('dir', 'null')->where('scopes', 'member')->where('config_name', 'in', $config_names[$tab_name])->select()->toArray();
                    if (count($config_names[$tab_name]) != count($config_list)) throw new Exception("与安装期望参数不符:在bw_config.php中{$tab_name}配置分类下实际配置数量与实际安装参数不一致");

                    //组装插入数据
                    foreach ($config_list as $config_init) {
                        //组装要插入的 应用初始化配置数据（member_id =0） 和 每个租户的配置数据
                        unset($config_init['id']);
                        $config_init['member_id'] = 0; //初始化配置的member_id=0
                        $config_init['tab_id'] = $tab_id;
                        $config_init['dir'] = $dir;
                        //插入初始化数据
                        $config_insert_list[] = $config_init;
                        //给每个租户插入配置
                        foreach ($member_ids as $member_id) {
                            $config_init['member_id'] = $member_id;
                            $config_insert_list[] = $config_init;
                        }
                    }
                }

            }
        }

        //组合数据
        if ($config_data) {  //判断应用是否存在组合数据
            if (isset($config_data['config_name']) && $config_data['config_name']) {
                //如果存在组合数据，进行租户插入逻辑
                $group_names = $config_data['config_name'];//得到所有的数据组标识数组 （一维数组 ， value是数据组标识 ）

                //根据数据组标识，得到该应用所有  安装sql 执行后插入的那些 初始化组合数据，并比对与配置文件中所写的那些数据组是否一致
                $group_ids = ConfigGroup::where('config_name', 'in', $group_names)->where('dir', 'null')->where('scopes', 'member')->column('id', 'config_name');
                if (count($group_names) != count($group_ids)) throw new Exception("与安装期望参数不符:在bw_data.php中组合数据数量与实际安装参数不一致");
                //遍历数据组，给租户插入组合数据
                foreach ($group_ids as $config_name => $group_id) {
                    //得到该数据组的全部初始化组合数据（member_id=0的数据）
                    $group_data_list = ConfigGroupData::where('member_id', 0)->where('dir', 'null')->where('scopes', 'member')->where('config_name', '=', $config_name)->select()->toArray();
                    //组装插入数据
                    foreach ($group_data_list as $group_data) {
                        //组装要插入的 应用初始化组合数据（member_id =0） 和 每个租户的组合数据
                        unset($group_data['id']);
                        $group_data['member_id'] = 0; //初始化组合数据的member_id=0
                        $group_data['group_id'] = $group_id;
                        $group_data['dir'] = $dir;
                        //插入初始化组合数据
                        $group_data_insert_list[] = $group_data;
                        //给每个租户插入组合数据
                        foreach ($member_ids as $member_id) {
                            $group_data['member_id'] = $member_id;
                            $group_data_insert_list[] = $group_data;
                        }
                    }
                }
            }
        }
        if ($trans) {
            Db::startTrans();
        }

        $res = $res1 = $res2 = $res3 = $res4 = true;
        try {
            //如果应用存在需要插入的配置分类
            if ($config_insert_list) {
                if ($config_tabs) {
                    //执行配置插入前，先删除安装sql插入的初始化配置（如果有），和该应用所有租户配置（如果有）
                    Config::where('tab_name', 'in', $config_tabs)->where('dir', $dir)->delete();
                    Config::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->delete();
                }

                //执行配置插入
                $configModel = new Config;
                $res2 = $configModel->saveAll($config_insert_list);
                if ($config_tabs) {
                    //更新配置分类dir标识为当前应用标识
                    ConfigTab::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->update(['dir' => $dir]);
                    Config::where('tab_name', 'in', $config_tabs)->where('dir', 'null')->update(['dir' => $dir]);
                }
            }
            //如果应用存在需要插入的组合数据
            if ($group_data_insert_list) {
                if ($group_names) {

                    //执行组合数据插入前，先删除安装sql插入的初始化组合数据（如果有），和该应用所有租户组合数据（如果有）
                    ConfigGroupData::where('config_name', 'in', $group_names)->where('dir', $dir)->delete();
                    ConfigGroupData::where('config_name', 'in', $group_names)->where('dir', 'null')->delete();
                }

                //执行组合数据插入
                $groupDataModel = new ConfigGroupData;
                $res4 = $groupDataModel->saveAll($group_data_insert_list);
                if ($group_names) {
                    //更新数据组dir标识为当前应用标识
                    ConfigGroup::where('config_name', 'in', $group_names)->where('dir', 'null')->update(['dir' => $dir]);
                    ConfigGroupData::where('config_name', 'in', $group_names)->where('dir', 'null')->update(['dir' => $dir]);
                }
            }

            $res = $res && $res1 && $res2 && $res3 && $res4 && true;

            if ($res) {
                if ($trans) {
                    Db::commit();
                }
            } else {
                if ($trans) {
                    Db::rollback();
                }
            }
        } catch (\Exception $e) {
            if ($trans) {
                Db::rollback();
            }
            throw new Exception($e->getMessage());
        }
        if (!$res) throw new Exception("安装配置和组合数据失败");


        return $res;
    }

    /**
     * 导出应用用户组数据至文件
     * @param string $dir 应用目录名
     */
    public static function exportMiniappGroup($dir)
    {
        //应用
        $miniapp = Miniapp::where('dir', $dir)->find();
        if (!$miniapp) return true;

        //功能模块
        $miniapp_modules = MiniappModule::alias('miniapp_module')
            ->where('miniapp_module.miniapp_id', $miniapp['id'])
            ->join('auth_group', 'miniapp_module.group_id = auth_group.id')
            ->field('auth_group.id,miniapp_module.group_id,miniapp_module.miniapp_id, miniapp_module.name, auth_group.group_name, miniapp_module.desc remark, miniapp_module.type module_type, miniapp_module.price module_price')
            ->order(['miniapp_module.type' => 'asc', 'miniapp_module.id' => 'asc'])
            ->select();
        if (!$miniapp_modules) return true;
        $miniapp_modules = $miniapp_modules->toArray();

        //应用根节点
        $root_node = AuthNode::where('pid', 0)->where('app_name', $dir)->where('type', 'app')->field('id, pid, title')->find();
        if (!$root_node) return true;

        foreach ($miniapp_modules as &$miniapp_module) {
            $auth_group = [];
            $nodes = AuthNode::alias('node')
                ->join('auth_group_node', 'node.id = auth_group_node.node_id')
                ->where([
                    ['auth_group_node.group_id', '=', $miniapp_module['group_id']],
                    ['node.pid', '<>', 0]
                ])
                ->field('node.id, node.pid, node.title, node.menu_path, node.name, node.auth_name, node.ismenu, node.param, node.target, node.icon, node.remark')
                ->order('node.sort desc, node.id asc')
                ->select();
            $nodes && $nodes = deal_list_to_tree($nodes->toArray(), 'id', 'pid', 'children', false, $root_node['id']);
            $miniapp_module['nodes'] = $nodes;
        }

        file_put_contents('menu.php', '<?php' . "\n\nreturn " . var_export($miniapp_modules, true) . ";");
        return true;
    }

    /**
     * 导出应用安装的配置和数据组文件
     */
    public static function exportMiniappConfig($dir, $dir_path, $un_dir_path)
    {

        //应用
        $miniapp = Miniapp::where('dir', $dir)->find();
        if (!$miniapp) return true;

        $config_tab_sql = '';//配置分类安装sql
        $config_sql = '';  //配置安装sql
        $group_sql = ''; //数据组安装sql
        $group_data_sql = ''; //数据组数据安装sql
        $install_sql = '';
        $uninstall_sql = '';

        $tableSql = self::getInstallSql($dir);
        //查询应用配置
        //查询应用配置分类
        $configTabs = ConfigTab::valiWhere('', null, $dir, false)->where('dir', $dir)->where('scopes', 'member')->where('type', 3)->column('tab_name');
        $configTabList = ConfigTab::valiWhere('', null, $dir, false)->where('dir', $dir)->where('scopes', 'member')->where('type', 3)->select()->toArray();
        if ($configTabs) {
            foreach ($configTabList as $tab) {
                unset($tab['id']);
                //得到配置分类插入sql
                $insertSQL = ConfigTab::valiWhere('', null, $dir, false)->fetchSql(true)->save($tab);
                $config_tab_sql .= $insertSQL . ';' . PHP_EOL;
            }
            //查询所有初始化配置
            $configs = Config::valiWhere('', null, $dir, false)->where('dir', $dir)->where('member_id', 0)->where('scopes', 'member')->where('tab_name', 'in', $configTabs)->select()->toArray();
            foreach ($configs as $config) {
                //得到配置值插入sql
                unset($config['id']);
                $insertSQL = Config::valiWhere('', null, $dir, false)->fetchSql(true)->save($config);
                $config_sql .= $insertSQL . ';' . PHP_EOL;
            }
        }
        //生成bwdata
        $configGroups = ConfigGroup::valiWhere('', null, $dir)->where('dir', $dir)->where('scopes', 'member')->column('config_name');
        $configGroupList = ConfigGroup::valiWhere('', null, $dir)->where('dir', $dir)->where('scopes', 'member')->select()->toArray();
        foreach ($configGroupList as $configGroup) {
            //得到数据组插入sql
            unset($configGroup['id']);
            $insertSQL = ConfigGroup::valiWhere('', null, $dir)->fetchSql(true)->save($configGroup);
            $group_sql .= $insertSQL . ';' . PHP_EOL;

        }
        //查询所有组合数据的初始化数据
        $configGroupDatas = ConfigGroupData::valiWhere('', null, $dir)->where('dir', $dir)->where('member_id', 0)->where('scopes', 'member')->where('config_name', 'in', $configGroups)->select()->toArray();
        foreach ($configGroupDatas as $configGroupData) {
            //得到数据组插入sql
            unset($configGroupData['id']);
            $insertSQL = ConfigGroupData::valiWhere('', null, $dir)->fetchSql(true)->save($configGroupData);
            $group_data_sql .= $insertSQL . ';' . PHP_EOL;

        }

        //组装表安装sql
        if ($tableSql['install']) $install_sql .= PHP_EOL . $tableSql['install'] . PHP_EOL;
        //组装配置的安装sql
        if ($config_tab_sql) $install_sql .= PHP_EOL . $config_tab_sql . PHP_EOL;
        if ($config_sql) $install_sql .= PHP_EOL . $config_sql . PHP_EOL;
        if ($group_sql) $install_sql .= PHP_EOL . $group_sql . PHP_EOL;
        if ($group_data_sql) $install_sql .= PHP_EOL . $group_data_sql . PHP_EOL;

        //修改表前缀
        $table_prefix = config('database.connections.mysql.prefix');
        //生成配置插入语句
        if ($install_sql) file_put_contents($dir_path . 'install.sql', str_replace($table_prefix, '__BWPREFIX__', $install_sql));
        //组装表卸载sql
        if ($tableSql['uninstall']) $uninstall_sql .= PHP_EOL . $tableSql['uninstall'] . PHP_EOL;
        //生成配置卸载sql
        if ($uninstall_sql) file_put_contents($un_dir_path . 'uninstall.sql', str_replace($table_prefix, '__BWPREFIX__', $uninstall_sql));

    }


    /**
     * 迁移应用静态文件
     */
    public static function exportMiniappStatic($dir, $target_dir = '')
    {
        //1.拷贝静态资源文件
        if (file_exists($dir)) {
            //拷贝模板到前台模板目录中去 public/static/demoApp/
            File::copy_dir($dir, $target_dir);
        }
    }


    /**
     * 迁移应用事件文件
     */
    public static function exportMiniappEventInit($target_dir = 'listen.php')
    {
        $event_ini = file_get_contents(root_path() . DS . 'buwang' . DS . 'template' . DS . 'event_ini.template.php');
        file_put_contents($target_dir, $event_ini);
    }


    /**
     * 导出应用用户组数据至文件
     * @param string $dir 应用目录名
     */
    public static function exportMiniappMenu($dir, $target_dir = 'menu.php')
    {
        //应用
        $miniapp = Miniapp::where('dir', $dir)->find();
        if (!$miniapp) return true;

        //功能模块 TODO:2021-7-6 14:40:28 应用自动生成的 菜单按照角色进行生成，不再按照套餐进行生成
//        $miniapp_modules = MiniappModule::alias('miniapp_module')
//            ->where('miniapp_module.miniapp_id', $miniapp['id'])
//            ->join('auth_group', 'miniapp_module.group_id = auth_group.id')
//            ->field('auth_group.id,miniapp_module.group_id,miniapp_module.miniapp_id, miniapp_module.name, auth_group.group_name, miniapp_module.desc remark, miniapp_module.type module_type, miniapp_module.price module_price')
//            ->order(['miniapp_module.type' => 'asc', 'miniapp_module.id' => 'asc'])
//            ->select();
        // id ，group_id 角色id miniapp_id 应用id  name,group_name 角色名   remark  备注  module_type 套餐类型 module_price 套餐价格

        $miniapp_modules = AuthGroup::field("id,id as group_id,{$miniapp['id']} miniapp_id,name,group_name,remark")->where('type', 'app')->where('app_name', $dir)->order(['id' => 'asc'])->select()->toArray();
        if (!$miniapp_modules) return true;
        //TODO 2021-7-6 14:42:56 生成前检查下每个角色是否存在重复节点
        $auth_group_ids = AuthGroup::where('app_name', $dir)->order(['id' => 'asc'])->column('id');
        if (!AuthGroup::checkGroupNodes($auth_group_ids)) throw new MiniappException(AuthGroup::getError(), 403);


        //应用根节点
        $root_node = AuthNode::where('pid', 0)->where('app_name', $dir)->where('type', 'app')->field('id, pid, title')->find();
        if (!$root_node) return true;

        foreach ($miniapp_modules as &$miniapp_module) {
            $auth_group = [];
            $nodes = AuthNode::alias('node')
                ->join('auth_group_node', 'node.id = auth_group_node.node_id')
                ->where([
                    ['auth_group_node.group_id', '=', $miniapp_module['group_id']],
                    ['node.pid', '<>', 0]
                ])
                ->field('node.id, node.pid, node.title, node.menu_path, node.name, node.auth_name, node.ismenu, node.param, node.target, node.icon, node.remark')
                ->order('node.sort desc, node.id asc')
                ->select();
            $nodes && $nodes = deal_list_to_tree($nodes->toArray(), 'id', 'pid', 'children', false, $root_node['id']);
            $miniapp_module['nodes'] = $nodes;
            unset($miniapp_module['id']);
            unset($miniapp_module['group_id']);
            unset($miniapp_module['miniapp_id']);
        }

        file_put_contents($target_dir, '<?php' . "\n\nreturn " . var_export($miniapp_modules, true) . ";");
        PluginService::filterMenu($target_dir);
        return true;
    }


    /**导出安装目录
     * @param $dir
     * @param string $target_dir
     */
    public static function exportPath($dir)
    {
        $dir_path = root_path() . 'public' . DS . $dir . DS . 'install' . DS;
        $un_dir_path = root_path() . 'public' . DS . $dir . DS . 'uninstall' . DS;
        $target_dir = root_path() . 'public' . DS . $dir . DS . 'install' . DS . 'static' . DS;
        return [
            'static' => $target_dir . '*',
            'group' => $dir_path . 'menu.php',
            'event' => $dir_path . 'listen.php',
            'install' => $dir_path . 'install.sql',
            'uninstall' => $un_dir_path . 'uninstall.sql',
            'app' => root_path() . DS . $dir . '======>' . root_path() . 'public' . DS . $dir,
            'zip' => '以上文件将被打包进：' . root_path() . 'public' . DS . $dir . '.zip',
        ];
    }


    /**导出安装目录
     * @param $dir
     * @param string $target_dir
     */
    public static function exportInstallDir($dir)
    {
        $dir_path = root_path() . 'public' . DS . $dir . DS . 'install';
        @mkdir($dir_path, 0777, true);
        $dir_path .= DS;

        $un_dir_path = root_path() . 'public' . DS . $dir . DS . 'uninstall';
        @mkdir($un_dir_path, 0777, true);
        $un_dir_path .= DS;

        $sorce_dir = root_path() . 'public' . DS . 'static' . DS . strtolower($dir) . DS;
        $target_dir = root_path() . 'public' . DS . $dir . DS . 'install' . DS . 'static' . DS;
        $event_ini = $dir_path . 'listen.php';
        $menu = $dir_path . 'menu.php';
        self::exportMiniappConfig($dir, $dir_path, $un_dir_path);//导入配置文件
        self::exportMiniappStatic($sorce_dir, $target_dir);//导入静态文件
        self::exportMiniappStatic($sorce_dir, $target_dir);//导入静态文件
        self::exportMiniappEventInit($event_ini);//导入钩子配置文件
        self::exportMiniappMenu($dir, $menu);//导入菜单配置
    }


    /**打包当前应用
     * @param $dir
     */
    public static function exportPackage($dir)
    {
        $dir_path = root_path() . 'public' . DS . $dir . DS . 'install';
        @mkdir($dir_path, 0777, true);
        $dir_path .= DS;

        $un_dir_path = root_path() . 'public' . DS . $dir . DS . 'uninstall';
        @mkdir($un_dir_path, 0777, true);
        $un_dir_path .= DS;

        $sorce_dir = root_path() . 'public' . DS . 'static' . DS . strtolower($dir) . DS;
        $target_dir = root_path() . 'public' . DS . $dir . DS . 'install' . DS . 'static' . DS;
        $sorce_app = root_path() . 'app' . DS . $dir . DS;
        $target_app = root_path() . 'public' . DS . $dir . DS;
        $zip_path = root_path() . 'public' . DS . $dir . '.zip';
        $event_ini = $dir_path . 'listen.php';
        $menu = $dir_path . 'menu.php';
        self::exportMiniappStatic($sorce_app, $target_app);//迁移项目文件
        self::exportMiniappConfig($dir, $dir_path, $un_dir_path);//导入配置文件
        self::exportMiniappStatic($sorce_dir, $target_dir);//导入静态文件
        self::exportMiniappEventInit($event_ini);//导入钩子配置文件
        self::exportMiniappMenu($dir, $menu);//导入菜单配置
        //项目打压缩包
        File::createSimpleZip($zip_path, $target_app);
        //删除生成文件夹
        File::del_dir($target_app);

    }

    /**得到所有表名
     * @param $database
     * @param string $dir
     */
    public static function list_tables($dir = '')
    {
        //TODO: 修复命令行生成应用安装配置取数据库错误
        $dbname = env('database.database', '');
        $prefix = env('database.prefix', 'bw_');//前缀
        $tables = Db::query("SHOW TABLES FROM  {$dbname}");
        //数据库中有哪些”
        $tabList = array();
        foreach ($tables as $table) {
            $table_name = $table["Tables_in_{$dbname}"];
            if ($dir) {
                if (preg_match("/^{$prefix}" . $dir . "+/", $table_name)) {
                    $tabList[] = $table_name;
                }
            } else {
                $tabList[] = $table_name;
            }
        }
        return $tabList;
    }

    /**得到安装sql和删除sql
     * @param string $dir
     */
    public static function getInstallSql($dir = '')
    {

        $install_list = '';
        $uninstall_list = '';
        //得到表名
        $tableNames = self::list_tables($dir);
        foreach ($tableNames as $tableName) {
            $tables = Db::query("show create table {$tableName}");
            //得到表结构
            if (isset($tables[0]['Create Table'])) {
                $info = "-- ----------------------------" . PHP_EOL;
                $info .= "-- Table structure for `{$tableName}`" . PHP_EOL;
                $info .= "-- ----------------------------" . PHP_EOL;
                $info .= "DROP TABLE IF EXISTS `{$tableName}` ;" . PHP_EOL;
                $sqlStr = $info . Util::setTableSqlCommonType($tables[0]['Create Table']) . ";\r\n\r\n";
                $install_list .= PHP_EOL . $sqlStr . PHP_EOL;
                $uninstall_list .= PHP_EOL . "DROP TABLE IF EXISTS `{$tableName}` ;" . PHP_EOL;
            }
        }
        return [
            'install' => $install_list,
            'uninstall' => $uninstall_list,
        ];
    }


    /**
     * 导出对应角色的安装menu.php
     * @param string $dir 表前缀
     */
    public static function exportGroup($miniapp_modules, $target_dir = 'menu.php')
    {
        foreach ($miniapp_modules as &$miniapp_module) {
            $auth_group = [];
            $nodes = AuthNode::alias('node')
                ->join('auth_group_node', 'node.id = auth_group_node.node_id')
                ->where([
                    ['auth_group_node.group_id', '=', $miniapp_module['group_id']],
                    ['node.pid', '<>', 0]
                ])
                ->field('node.id, node.pid, node.title, node.menu_path, node.name, node.auth_name, node.ismenu, node.param, node.target, node.icon, node.remark')
                ->order('node.sort desc, node.id asc')
                ->select();
            $nodes && $nodes = deal_list_to_tree($nodes->toArray(), 'id', 'pid', 'children', false, $miniapp_module['root_id']);
            $miniapp_module['nodes'] = $nodes;
        }
        file_put_contents($target_dir, '<?php' . "\n\nreturn " . var_export($miniapp_modules, true) . ";");
        return true;
    }


    /**
     * 安装应用配置和租户数据( 应用数据分离版),只做基础数据插入
     * @param string $dir
     * @return array|mixed
     */
    public static function installConfigAndGroupDataPlus($dir, $trans = false)
    {
        if ($trans) {
            Db::startTrans();
        }
        try {
            try {
                //如果应用存在需要插入的配置分类
                $config_tab_ids = ConfigTab::valiWhere('', null, $dir, false)->where('dir', $dir)->where('scopes', 'member')->column('id', 'tab_name');
            } catch (\Exception $e) {
                //TODO  JYK 20210701 修复：如果该应用未使用任何应用配置也没有创建应用配置表不能算报错
                $config_tab_ids = [];
            }
            //遍历配置分类，给租户插入配置数据
            foreach ($config_tab_ids as $tab_name => $tab_id) {
                //修正分类id参数
                Config::valiWhere('', null, $dir, false)->where('tab_name', '=', $tab_name)->where('dir', $dir)->update(['tab_id' => $tab_id]);
            }

            //如果存在组合数据，进行租户插入逻辑
            try {
                //如果应用存在需要插入的配置分类
                $group_ids = ConfigGroup::valiWhere('', null, $dir)->where('dir', $dir)->where('scopes', 'member')->column('id', 'config_name');
            } catch (\Exception $e) {
                //TODO  JYK 20210701 修复：如果该应用未使用任何组合数据也没有创建组合数据表不能算报错
                $group_ids = [];
            }
            //遍历数据组，给租户插入组合数据
            foreach ($group_ids as $config_name => $group_id) {
                //修正数据组id参数
                ConfigGroupData::valiWhere('', null, $dir)->where('config_name', '=', $config_name)->where('dir', $dir)->update(['group_id' => $group_id]);
            }

            if ($trans) {
                Db::commit();
            }
        } catch (\Exception $e) {
            if ($trans) {
                Db::rollback();
            }
            throw new Exception("安装配置和组合数据失败：" . $e->getMessage());
        }

        return true;
    }


    /**
     * 初始化租户配置和数据组（购买时）
     * @param int $member_id 租户用户ID
     * @param string $dir 应用目录名
     * @param boolean $trans 是否开启事务
     * @return void
     * @throws Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public static function initMemberConfigAndData($member_id, $dir, $trans = false)
    {
        if ($trans) {
            Db::startTrans();
        }
        $config_data = $group_data = [];
        //得到租户的初始化数据
        try {
            //TODO 获取config_tab表应用默认配置分类
            $tab_ids = ConfigTab::where('type', 5)->where('dir', $dir)->where('scopes', 'member')->column('id');
            //获取初始化默认数据，如果$dir为真，会处理应用相关的配置表
            $config_list = Config::valiWhere('', null, $dir, false)->where('member_id', 0)->where('tab_id', 'in', $tab_ids)->select()->toArray();
        } catch (\Exception $e) {
            //TODO 20210701 修复：如果该应用未使用任何应用配置也没有创建应用配置表不能算报错
            $config_list = [];
        }

        try {
            //组合数据
            $config_data_list = ConfigGroupData::valiWhere('', null, $dir)->where('member_id', 0)->where('scopes', 'member')->where('dir', '=', $dir)->select()->toArray();
        } catch (\Exception $e) {
            //TODO  20210701 修复：如果该应用未使用任何应用配置也没有创建应用配置表不能算报错
            $config_data_list = [];
        }

        //组装提交数据
        //租户配置
        if (!empty($config_list)) {
            foreach ($config_list as $config) {
                $member_config = Config::valiWhere('', null, $dir, false)->where('member_id', $member_id)->where('scopes', 'member')->where('dir', '=', $dir)->where('config_name', $config['config_name'])->find();
                if (!$member_config) {
                    unset($config['id']);
                    $config['member_id'] = $member_id;
                    $config_data[] = $config;
                }
            }
        }
        //租户组合数据
        if (!empty($config_data_list)) {
            foreach ($config_data_list as $data) {
                $member_data = ConfigGroupData::valiWhere('', null, $dir)->where('member_id', $member_id)->where('scopes', 'member')->where('dir', '=', $dir)->where('config_name', $data['config_name'])->find();
                if (!$member_data) {
                    unset($data['id']);
                    $data['member_id'] = $member_id;
                    $group_data[] = $data;
                }
            }
        }

        try {
            //执行提交
            if ($config_data) { //配置
                $config = Config::valiWhere('', null, $dir, false);
                $config->saveAll($config_data);
            }
            if ($group_data) { //组合数据
                $data = ConfigGroupData::valiWhere('', null, $dir);
                $data->saveAll($group_data);
            }
            if ($trans) {
                Db::commit();
            }
        } catch (\Exception $e) {
            if ($trans) {
                Db::rollback();
            }
            throw new Exception("安装配置和组合数据失败：" . $e->getMessage());
        }

    }

    public static function checkMiniappExpire($memberMiniapp)
    {
        //基础功能
        $miniappModule = MiniappModule::where('miniapp_id', $memberMiniapp['miniapp_id'])->where('type', 1)->find();
        if (!$miniappModule) throw new MiniappException('该应用缺少基础功能', 403);
        //得到最新购买订单的过期时间
        $memberMiniappOrder = MemberMiniappOrder::where('member_id', $memberMiniapp['member_id'])->where('miniapp_id', $memberMiniapp['miniapp_id'])->where('miniapp_module_id', $miniappModule['id'])->order('create_time desc')->find();
        if (!$memberMiniappOrder) throw new MiniappException('该应用缺少购买记录', 403);
        if ($memberMiniappOrder['expire_time'] == 0) return true; //永久
        if (time() >= $memberMiniappOrder['expire_time']) throw new MiniappException('该应用已过期，暂无法访问，请续费', 403);
        return true;
    }


}